package top.cardone.generator.code;

import com.google.common.base.CaseFormat;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapperResultSetExtractor;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import top.cardone.context.ApplicationContextHolder;
import top.cardone.generator.code.mapper.FieldMapper;
import top.cardone.generator.code.mapper.PoMapper;
import top.cardone.generator.code.template.RunTemplate;

import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * PO对象映射工厂
 *
 * @author yao hai tao
 */
@lombok.Getter
@lombok.Setter
@lombok.experimental.Accessors(chain = true)
@lombok.extern.log4j.Log4j2
public class PoMapperFactory {
    private String defaultFieldType = "Object";

    private String productName;

    private RunTemplate runTemplate;

    private String schemaPattern;

    private Map<String, String> typeMap;

    private String typeMapBeanId = "fieldTojavaTypeMap";

    private Map<String, Object> variables;

    List<PoMapper> poMapperList = null;

    /**
     * 查询实体
     *
     * @param dbmd      DatabaseMetaData
     * @param tableName 表名
     * @return 实体
     * @throws Exception 异常
     */
    private PoMapper findPoMapper(final DatabaseMetaData dbmd, final String tableName) throws Exception {
        if (poMapperList == null) {
            try (ResultSet rs = dbmd.getTables(null, this.schemaPattern, null, new String[]{"TABLE"})) {
                final RowMapperResultSetExtractor<PoMapper> rowMapperResultSetExtractor = new RowMapperResultSetExtractor<>(BeanPropertyRowMapper.newInstance(PoMapper.class), 0);

                poMapperList = rowMapperResultSetExtractor.extractData(rs);
            } catch (Exception ex) {
                poMapperList = Lists.newArrayList();

                log.error(ex);
            }
        }

        PoMapper poMapper = poMapperList.stream().filter(it -> StringUtils.equalsAnyIgnoreCase(it.getTableName(), tableName)).findFirst().orElseGet(() -> null);

        Assert.notNull(poMapper, "not exists " + tableName);

        log.info("load " + tableName);

        if (StringUtils.isBlank(poMapper.getRemarks())) {
            this.initPoMapperRemarks(tableName, poMapper);
        }

        this.initPoMapperFields(dbmd, tableName, poMapper);

        return poMapper;
    }

    /**
     * 初始化
     */
    public void init() {
        Assert.notNull(this.defaultFieldType, "defaultFieldType is null");

        Assert.notNull(this.runTemplate, "runTemplate is null");

        if (MapUtils.isEmpty(this.typeMap)) {
            this.typeMap = ApplicationContextHolder.getBean(Map.class, this.typeMapBeanId);
        }

        Assert.notNull(this.typeMap, "typeMap is null");
    }


    List<FieldMapper> allFieldMapperList = null;

    /**
     * 初始化实体字段
     *
     * @param dbmd      DatabaseMetaData
     * @param tableName 表名
     * @param poMapper  实体对象
     * @throws SQLException 异常
     */
    private void initPoMapperFields(final DatabaseMetaData dbmd, final String tableName, final PoMapper poMapper) throws SQLException {
        if (allFieldMapperList == null) {
            try (ResultSet rs = dbmd.getColumns(null, this.schemaPattern, null, null)) {
                final RowMapperResultSetExtractor<FieldMapper> rowMapperResultSetExtractor = new RowMapperResultSetExtractor<>(BeanPropertyRowMapper.newInstance(FieldMapper.class), 0);

                allFieldMapperList = rowMapperResultSetExtractor.extractData(rs);
            } catch (Exception ex) {
                allFieldMapperList = Lists.newArrayList();

                log.error(ex);
            }
        }

        List<FieldMapper> fieldMapperList = allFieldMapperList.stream().filter(it -> StringUtils.equalsAnyIgnoreCase(it.getTableName(), tableName)).collect(Collectors.toList());

        Map<String, FieldMapper> fieldMapperMap = Maps.newTreeMap();

        for (final FieldMapper fieldMapper : fieldMapperList) {
            final String code = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, fieldMapper.getColumnName());

            final String namePascalCase = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, fieldMapper.getColumnName());

            fieldMapper.setCode(code);
            fieldMapper.setName(code);
            fieldMapper.setNamePascalCase(namePascalCase);

            if (fieldMapperMap == null) {
                fieldMapperMap = Maps.newTreeMap();
            }

            String typeCode = this.typeMap.get(String.valueOf(fieldMapper.getDataType()));

            typeCode = StringUtils.defaultIfBlank(typeCode, this.defaultFieldType);

            fieldMapper.setTypeCode(typeCode);

            fieldMapperMap.put(code, fieldMapper);
        }

        for (final FieldMapper fieldMapper : fieldMapperMap.values()) {
            if (StringUtils.isNotBlank(fieldMapper.getRemarks())) {
                continue;
            }

            String remarks = fieldMapper.getRemarks();

            if ("Oracle".equals(this.productName)) {
                String sql = "SELECT COMMENTS FROM USER_COL_COMMENTS WHERE TABLE_NAME= ? AND COLUMN_NAME = ?";

                remarks = ApplicationContextHolder.getBean(JdbcTemplate.class).queryForList(sql, new Object[]{tableName, fieldMapper.getColumnName()}, String.class).stream().findFirst().orElseGet(() -> "");
            } else if ("MySQL".equals(this.productName)) {
                String sql = "SELECT IF(T.COLUMN_COMMENT = '' OR T.COLUMN_COMMENT IS NULL, T.COLUMN_NAME, T.COLUMN_COMMENT) FROM INFORMATION_SCHEMA.COLUMNS T WHERE T.TABLE_SCHEMA = ? AND T.TABLE_NAME = ? AND T.COLUMN_NAME = ?";

                remarks = ApplicationContextHolder.getBean(JdbcTemplate.class).queryForList(sql, new Object[]{this.schemaPattern, tableName, fieldMapper.getColumnName()}, String.class).stream().findFirst().orElseGet(() -> "");
            } else if (StringUtils.equalsAnyIgnoreCase("PostgreSQL", this.productName) || StringUtils.equalsAnyIgnoreCase("postgres", this.productName)) {
                String sql = "select d.description from pg_description d join pg_attribute a on (a.attnum = d.objsubid) join pg_class c on (c.oid = d.objoid and c.oid = a.attrelid) join pg_namespace n on (n.oid = c.relnamespace)  where n.nspname = ? and c.relname = ? and a.attname = ?";

                remarks = ApplicationContextHolder.getBean(JdbcTemplate.class).queryForList(sql, new Object[]{this.schemaPattern, tableName, fieldMapper.getColumnName()}, String.class).stream().findFirst().orElseGet(() -> "");
            }

            fieldMapper.setRemarks(StringUtils.defaultString(remarks, fieldMapper.getColumnName()));
        }

        poMapper.setFieldMapperMap(fieldMapperMap);

        this.initPoMapperPrimaryKeys(dbmd, tableName, fieldMapperMap);
    }

    /**
     * 初始化实体属性
     *
     * @param dbmd DatabaseMetaData
     */
    private void initPoMapperMap(final DatabaseMetaData dbmd) {
        final List<PoMapper> poMapperList = this.runTemplate.findListPoMapper();

        if (CollectionUtils.isEmpty(poMapperList)) {
            return;
        }

        for (final PoMapper poMapper : poMapperList) {
            final String code = poMapper.getCode();

            PoMapper dbPoMapper = null;

            try {
                dbPoMapper = this.findPoMapper(dbmd, poMapper.getTableName());
            } catch (final Exception e) {
                log.error(e);
            }

            if (dbPoMapper == null) {
                continue;
            }

            BeanUtils.copyProperties(dbPoMapper, poMapper);

            poMapper.setCode(code);
        }
    }

    List<FieldMapper> allPrimaryKeysFieldMapperList;

    /**
     * 初始化实体主键
     *
     * @param dbmd           DatabaseMetaData
     * @param tableName      表名
     * @param fieldMapperMap 字段映射
     * @throws SQLException 异常
     */
    private void initPoMapperPrimaryKeys(final DatabaseMetaData dbmd, final String tableName, final Map<String, FieldMapper> fieldMapperMap) throws SQLException {
        if (allPrimaryKeysFieldMapperList == null) {
            try (ResultSet rs = dbmd.getPrimaryKeys(null, this.schemaPattern, null)) {
                final RowMapperResultSetExtractor<FieldMapper> rowMapperResultSetExtractor = new RowMapperResultSetExtractor<>(BeanPropertyRowMapper.newInstance(FieldMapper.class), 0);

                allPrimaryKeysFieldMapperList = rowMapperResultSetExtractor.extractData(rs);
            } catch (Exception ex) {
                allPrimaryKeysFieldMapperList = Lists.newArrayList();

                log.error(ex);
            }
        }

        if (CollectionUtils.isEmpty(allPrimaryKeysFieldMapperList)) {
            return;
        }

        List<FieldMapper> primaryKeysFieldMapperList = allPrimaryKeysFieldMapperList.stream().filter(it -> StringUtils.equalsAnyIgnoreCase(it.getTableName(), tableName)).collect(Collectors.toList());

        if (CollectionUtils.isEmpty(primaryKeysFieldMapperList)) {
            return;
        }

        for (final FieldMapper primaryKeysFieldMapper : primaryKeysFieldMapperList) {
            final String code = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, primaryKeysFieldMapper.getColumnName());

            final FieldMapper fieldMapper = fieldMapperMap.get(code);

            fieldMapper.setKeySeq(primaryKeysFieldMapper.getKeySeq());
            fieldMapper.setPkName(primaryKeysFieldMapper.getPkName());
        }
    }

    /**
     * 初始化实体说明
     *
     * @param tableName 表名
     * @param poMapper  实体对象
     * @throws MetaDataAccessException 异常
     */
    private void initPoMapperRemarks(final String tableName, final PoMapper poMapper) throws MetaDataAccessException {
        this.productName = JdbcUtils.extractDatabaseMetaData(ApplicationContextHolder.func(JdbcTemplate.class, jdbcTemplate -> jdbcTemplate.getDataSource()), "getDatabaseProductName").toString();

        this.productName = JdbcUtils.commonDatabaseName(this.productName);

        try {
            String remarks = poMapper.getRemarks();

            if ("Oracle".equals(this.productName)) {
                String sql = "SELECT NVL(COMMENTS, TABLE_NAME) FROM USER_TAB_COMMENTS WHERE TABLE_NAME= ?";

                remarks = ApplicationContextHolder.getBean(JdbcTemplate.class).queryForList(sql, new Object[]{tableName}, String.class).stream().findFirst().orElseGet(() -> "");
            } else if ("MySQL".equals(this.productName)) {
                String sql = "select IF(T.TABLE_COMMENT = '' OR T.TABLE_COMMENT IS NULL, T.table_name, T.TABLE_COMMENT) from INFORMATION_SCHEMA.tables t where t.table_schema = ? and t.table_name = ?";

                remarks = ApplicationContextHolder.getBean(JdbcTemplate.class).queryForList(sql, new Object[]{this.schemaPattern, tableName}, String.class).stream().findFirst().orElseGet(() -> "");
            } else if (StringUtils.equalsAnyIgnoreCase("PostgreSQL", this.productName) || StringUtils.equalsAnyIgnoreCase("postgres", this.productName)) {
                String sql = "select d.description from pg_description d join pg_class c on (c.oid = d.objoid) join pg_namespace n on (n.oid = c.relnamespace) where n.nspname = ? and c.relname = ? and d.objsubid = 0";

                remarks = ApplicationContextHolder.getBean(JdbcTemplate.class).queryForList(sql, new Object[]{this.schemaPattern, tableName}, String.class).stream().findFirst().orElseGet(() -> "");
            }

            poMapper.setRemarks(StringUtils.defaultString(remarks, poMapper.getTableName()));
        } catch (final Exception e) {
            log.error("not exists：" + tableName);

            log.error(e);
        }
    }

    /**
     * 执行
     *
     * @throws Exception 异常
     */
    public void run() throws Exception {
        JdbcUtils.extractDatabaseMetaData(ApplicationContextHolder.func(JdbcTemplate.class, jdbcTemplate -> jdbcTemplate.getDataSource()), dbmd -> {
            PoMapperFactory.this.initPoMapperMap(dbmd);

            return null;
        });

        this.runTemplate.run();
    }
}
